﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Threading;

namespace Amnesia.Data
{
	/// <summary>
	/// Database connections participating in the distributed transaction started by
	/// the Amnesia session should be shared across web request. This class makes it
	/// easier to add the functionality to an existing application's data access layer.
	/// Or you can roll your on version of this.
	/// </summary>
	public class ConnectionHelper
	{
		static Dictionary<string, IDbConnection> ambientConnections = new Dictionary<string, IDbConnection>();

		/// <summary>
		/// Gets or creates a connection that can be used with the current ambient transaction (System.Transactions.Transaction.Current).
		/// If no ambient transaction is present, null is returned and your application's normal connection management code should
		/// be used.  If the transaction is distributed transaction then the connection can safely be shared across threads if needed.
		/// </summary>
		/// <param name="connectionString">Database connection string to get the connection object for</param>
		/// <param name="createConnection">If the connection has not yet been created and there is an active transaction
		/// then this factory method will be used to create a new connection.</param>
		public static IDbConnection GetAmbientConnection(string connectionString, Func<string, IDbConnection> createConnection)
		{
			if (!Session.IsActive)
				return null;

			IDbConnection connection = null;

			// If there's a existing TransactionScope then try to reuse connections.
			// This is needed if distributed transactions are used across threads.
			var ambientTx = System.Transactions.Transaction.Current;

			if (ambientTx != null)
			{
				string key;

				if (ambientTx.TransactionInformation.DistributedIdentifier != null)
					key = ambientTx.TransactionInformation.DistributedIdentifier.ToString();
				else
					key = ambientTx.TransactionInformation.LocalIdentifier;

				key += "|" + connectionString;

				lock (ambientConnections)
				{

					if (!ambientConnections.TryGetValue(key, out connection))
					{
						connection = createConnection(connectionString);
						ambientConnections[key] = connection;

						ambientTx.TransactionCompleted += delegate
						{
							lock (ambientConnections)
								ambientConnections.Remove(key);

							if (connection.State == ConnectionState.Open)
								connection.Close();
						};
					}


				}
			}

			return connection;
		}

		/// <summary>
		/// Because ambient connections from distributed transactions can be shared across threads
		/// access to them must be synchronized.  For normal connections that are not part of a distributed
		/// transaction no locking is performed for better performance.  To control access to a connection,
		/// put all calls to ExecuteReader, etc inside of a using block:  using(LockConnection(conn)) { query }
		/// </summary>
		public static IDisposable LockConnection(IDbConnection connection)
		{
			var ambientTx = System.Transactions.Transaction.Current;
			if (ambientTx == null || ambientTx.TransactionInformation.DistributedIdentifier == null)
				return NullDisposable.Instance;

			Monitor.Enter(connection);

			return new UndoableAction(delegate
			{
				Monitor.Exit(connection);
			});
		}

		/// <summary>
		/// Disconnects the reader if there is an active Amnesia session.
		/// </summary>
		/// <param name="reader">Reader that was just returned by ExecuteRedaer</param>
		public static IDataReader PrepareReader(IDataReader reader)
		{
			return Session.IsActive ? new DisconnectedReader(reader) : reader;
		}

		#region UndoableAction
		/// <summary>
		/// Implementation of IDisposible that invokes a delegate
		/// when the object is disposed.
		/// </summary>
		class UndoableAction : IDisposable
		{
			public delegate void Undo();

			Undo undo;

			public UndoableAction(Undo undo)
			{
				this.undo = undo;
			}

			public void Dispose()
			{
				if (undo != null)
				{
					undo();
					undo = null;
				}
			}
		}
		#endregion

		#region NullDisposable
		class NullDisposable : IDisposable
		{
			public static IDisposable Instance = new NullDisposable();

			private NullDisposable()
			{
			}

			void IDisposable.Dispose()
			{
			}
		}
		#endregion
	}
}
